﻿using System.Collections.Generic;
#if NETFX_CORE
using System.Composition.Hosting;
#elif !WINDOWS_PHONE
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
#endif
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Threading.Tasks;
using DaveSexton.Labs.Properties;

namespace DaveSexton.Labs
{
	/// <summary>
	/// Executes <see cref="Lab"/> instances and optionally discovers labs automatically.
	/// </summary>
	/// <remarks>
	/// - Typically provides a UI for choosing labs to be executed.
	/// - Discovery or Catalog
	/// 
	/// Automatically discovers types that derive from <see cref="Lab"/> within the assembly
	/// in which the type that derives from <see cref="LabController"/> is defined.
	/// 
	/// ..Catalog can create instances of labs to be executed..
	/// </remarks>
	[ContractClass(typeof(LabControllerContract))]
	public abstract class LabController : LabTraceListener
	{
		#region Public Properties
		#endregion

		#region Private / Protected
		protected string ApplicationTitle
		{
			get
			{
				Contract.Ensures(Contract.Result<string>() != null);

				return applicationTitle ?? string.Empty;
			}
		}

		private readonly LabFactory factory;
		private string applicationTitle;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="LabController" /> class for derived classes.
		/// </summary>
		protected LabController()
			: this(new DefaultLabCatalog())
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="LabController" /> class for derived classes.
		/// </summary>
		protected LabController(LabCatalog catalog)
		{
			Contract.Requires(catalog != null);

			this.factory = new LabFactory(catalog);
		}
		#endregion

		#region Methods
		[ContractInvariantMethod]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
		private void ObjectInvariant()
		{
			Contract.Invariant(factory != null);
		}

		protected static string Format(string message, params object[] args)
		{
			Contract.Ensures(Contract.Result<string>() != null);

			return LabTraceSource.Format(message, args);
		}

		private void AttachGlobalListener()
		{
			var listeners = LabTraceSource.Default.Listeners;

#if !SILVERLIGHT
			Contract.Assume(listeners != null);
#endif

			if (!listeners.Contains(this))
			{
				listeners.Add(this);
			}

#if !SILVERLIGHT && !NETFX_CORE
			if (!Trace.Listeners.Contains(this))
			{
				Trace.Listeners.Add(this);
			}
#endif
		}

		internal void DetachGlobalListener()
		{
			var listeners = LabTraceSource.Default.Listeners;

#if !SILVERLIGHT
			Contract.Assume(listeners != null);
#endif

			listeners.Remove(this);

#if !SILVERLIGHT && !NETFX_CORE
			Trace.Listeners.Remove(this);
#endif
		}

#if !WINDOWS_PHONE
#if NETFX_CORE
		public void Start(Assembly entryAssembly)
		{
			Contract.Requires(entryAssembly != null);
#else
		public void Start()
		{
#endif
#if NETFX_CORE
			var discoveryCatalog = new ContainerConfiguration().WithAssembly(entryAssembly);
#elif !SILVERLIGHT
			var assembly = Assembly.GetEntryAssembly();

			Contract.Assume(assembly != null);
			Contract.Assume(!assembly.ReflectionOnly);

			using (var discoveryCatalog = new AssemblyCatalog(assembly))
#else
			using (var discoveryCatalog = new AssemblyCatalog(Assembly.GetCallingAssembly()))
#endif
			{
#if NETFX_CORE
				Start(entryAssembly, discoveryCatalog);
#else
				Start(discoveryCatalog);
#endif
			}
		}

#if NETFX_CORE
		public void Start(Assembly entryAssembly, ContainerConfiguration discoveryCatalog)
		{
			Contract.Requires(entryAssembly != null);
#else
		public void Start(ComposablePartCatalog discoveryCatalog)
		{
#endif
			Contract.Requires(discoveryCatalog != null);

			ICollection<Lab> labs = factory.LoadLabs(discoveryCatalog);

#if !NETFX_CORE
			Assembly entryAssembly = null;

			if (!(discoveryCatalog is AssemblyCatalog))
			{
#if !SILVERLIGHT
				entryAssembly = Assembly.GetEntryAssembly();
#else
				entryAssembly = Assembly.GetCallingAssembly();
#endif
			}
#endif

			TraceHeader(entryAssembly, discoveryCatalog, labs.Count);

			if (labs.Count == 0)
			{
				ZeroLabs();
			}
			else
			{
				AttachGlobalListener();

				MainMenu(labs);

				WriteLine();
			}
		}

#if NETFX_CORE
		public async Task StartDebugAsync(Assembly entryAssembly)
		{
			Contract.Requires(entryAssembly != null);
#else
		public Task StartDebugAsync()
		{
#endif
#if NETFX_CORE
			var discoveryCatalog = new ContainerConfiguration().WithAssembly(entryAssembly);
#elif !SILVERLIGHT
			var assembly = Assembly.GetEntryAssembly();

			Contract.Assume(assembly != null);
			Contract.Assume(!assembly.ReflectionOnly);

			using (var discoveryCatalog = new AssemblyCatalog(assembly))
#else
			using (var discoveryCatalog = new AssemblyCatalog(Assembly.GetCallingAssembly()))
#endif
			{
#if NETFX_CORE
				return StartDebugAsync(entryAssembly, discoveryCatalog);
#else
				return StartDebugAsync(discoveryCatalog);
#endif
			}
		}

#if NETFX_CORE
		public async Task StartDebugAsync(Assembly entryAssembly, ContainerConfiguration discoveryCatalog)
		{
			Contract.Requires(entryAssembly != null);
#else
		public async Task StartDebugAsync(ComposablePartCatalog discoveryCatalog)
		{
#endif
			Contract.Requires(discoveryCatalog != null);

			ICollection<Lab> labs = factory.LoadLabs(discoveryCatalog);

#if !NETFX_CORE
			Assembly entryAssembly = null;

			if (!(discoveryCatalog is AssemblyCatalog))
			{
#if !SILVERLIGHT
				entryAssembly = Assembly.GetEntryAssembly();
#else
				entryAssembly = Assembly.GetCallingAssembly();
#endif
			}
#endif

			TraceHeader(entryAssembly, discoveryCatalog, labs.Count);

			if (labs.Count == 0)
			{
				ZeroLabs();
			}
			else
			{
				AttachGlobalListener();

				await ExecuteAsync(labs, showDescription: true, showSourceCode: false);

				Contract.Assume(labs.Count > 0);

				EndDebug(labs);
			}
		}
#else
		public void Start()
		{
			Assembly entryAssembly = Assembly.GetCallingAssembly();

			ICollection<Lab> labs = factory.LoadLabs(entryAssembly);

			TraceHeader(entryAssembly, labs.Count);

			if (labs.Count == 0)
			{
				ZeroLabs();
			}
			else
			{
				AttachGlobalListener();

				MainMenu(labs);

				WriteLine();
			}
		}

		public async Task StartDebugAsync()
		{
			Assembly entryAssembly = Assembly.GetCallingAssembly();

			ICollection<Lab> labs = factory.LoadLabs(entryAssembly);

			TraceHeader(entryAssembly, labs.Count);

			if (labs.Count == 0)
			{
				ZeroLabs();
			}
			else
			{
				AttachGlobalListener();

				await ExecuteAsync(labs, showDescription: true, showSourceCode: false);

				Contract.Assume(labs.Count > 0);

				EndDebug(labs);
			}
		}
#endif

		protected abstract void ZeroLabs();

		protected abstract void MainMenu(ICollection<Lab> labs);

		protected abstract void EndDebug(ICollection<Lab> labs);

#if !WINDOWS_PHONE
#if NETFX_CORE
		private void TraceHeader(Assembly entryAssembly, ContainerConfiguration discoveryCatalog, int labCount)
#else
		private void TraceHeader(Assembly entryAssembly, ComposablePartCatalog discoveryCatalog, int labCount)
#endif
		{
			Contract.Requires(discoveryCatalog != null);
			Contract.Requires(labCount > -1);

#if NETFX_CORE
			var assembly = entryAssembly;
#else
			var assemblyCatalog = discoveryCatalog as AssemblyCatalog;

			Assembly assembly = assemblyCatalog == null ? entryAssembly : assemblyCatalog.Assembly;
#endif

			Contract.Assume(assembly != null);

			TraceHeader(assembly, labCount);
		}
#endif

		private void TraceHeader(Assembly assembly, int labCount)
		{
			Contract.Requires(assembly != null);
			Contract.Requires(labCount > -1);

#if !SILVERLIGHT
			AssemblyName assemblyInfo = assembly.GetName();
#else
			AssemblyName assemblyInfo = new AssemblyName(assembly.FullName);
#endif

			if (!assembly.TryGetAssemblyTitle(out applicationTitle))
				applicationTitle = assemblyInfo.Name;

			TraceInformation(Resources.WelcomeFormat, applicationTitle, assemblyInfo.Version);

			string copyright;

			if (assembly.TryGetAttributeValue<AssemblyCopyrightAttribute>(a => a.Copyright, out copyright))
				TraceInformation(copyright);

			TraceInformation(Resources.DiscoveredLabsCountFormat, labCount);
		}

		protected virtual async Task ExecuteAsync(ICollection<Lab> labs, bool showDescription, bool showSourceCode)
		{
			Contract.Requires(labs != null);
			Contract.Requires(labs.Count > 0);
			////Contract.Requires(Contract.ForAll(labs, lab => lab != null));
			////Contract.Requires(Contract.ForAll(labs, lab => lab.IsEnabled));

			int total = labs.Count;
			int number = 0;

			foreach (var lab in labs)
			{
				Contract.Assume(lab != null);

				await ExecuteAsync(lab, ++number, total, showDescription, showSourceCode);
			}

			WriteLine();
			WriteLine(Resources.AllLabsCompleted);
		}

		protected async Task<bool> ExecuteAsync(Lab lab, int number, int total, bool showDescription, bool showSourceCode)
		{
			Contract.Requires(lab != null);
			Contract.Requires(number > 0);
			Contract.Requires(total > 0);

			LabStarting(lab, number, total);

			if (lab.CanExecute())
			{
				if (lab.IsCancellable)
					Write(Resources.PressAnyKeyToCancel_Inline);

				TraceLabDescriptionIf(showDescription, lab.Description);

				if (showSourceCode)
					TraceSourceCode(lab.SourceCode);

				await ExecuteAsync(lab);

				LabCompleted(lab, number, total, executed: true);

				return true;
			}
			else
			{
				TraceWarning(Resources.Skipped_Inline);

				LabCompleted(lab, number, total, executed: false);

				return false;
			}
		}

		protected virtual async Task ExecuteAsync(Lab lab)
		{
			Contract.Requires(lab != null);
			Contract.Requires(lab.CanExecute());

			var listeners = lab.Listeners;

#if !SILVERLIGHT
			Contract.Assume(listeners != null);
#endif

			listeners.Add(this);

			try
			{
				Contract.Assume(lab.CanExecute());

				await lab.ExecuteAsync();
			}
			finally
			{
				listeners.Remove(this);
			}
		}

		protected virtual void LabStarting(Lab lab, int number, int total)
		{
			Contract.Requires(lab != null);
			Contract.Requires(number > 0);
			Contract.Requires(total > 0);

			// for derived classes
		}

		protected virtual void TraceSourceCode(string sourceCode)
		{
			if (!string.IsNullOrEmpty(sourceCode))
			{
				WriteLine();
				WriteLine(sourceCode);
			}
		}

		protected virtual void LabCompleted(Lab lab, int number, int total, bool executed)
		{
			Contract.Requires(lab != null);
			Contract.Requires(number > 0);
			Contract.Requires(total > 0);

			// for derived classes
		}

		protected abstract void TraceLabDescriptionIf(bool trace, string description);

		public sealed override void Write(string message)
		{
			Write(message, TraceEventType.Information);
		}

		public sealed override void WriteLine(string message)
		{
			WriteLine(message, TraceEventType.Information);
		}

		public abstract void Write(string message, TraceEventType eventType);

		public abstract void WriteLine(string message, TraceEventType eventType);

		public void WriteLine()
		{
			WriteLine(string.Empty);
		}

		public void TraceWarning(string message)
		{
			TraceEvent(null, null, TraceEventType.Warning, 0, message ?? string.Empty);
		}

		public void TraceWarning(string format, params object[] args)
		{
			TraceEvent(null, null, TraceEventType.Warning, 0, LabTraceSource.Format(format, args));
		}

		public void TraceInformation(string message)
		{
			TraceEvent(null, null, TraceEventType.Information, 0, message ?? string.Empty);
		}

		public void TraceInformation(string format, params object[] args)
		{
			TraceEvent(null, null, TraceEventType.Information, 0, LabTraceSource.Format(format, args));
		}

		public sealed override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
		{
			TraceEvent(eventCache, source, eventType, id, LabTraceSource.Format(format, args));
		}

		protected override void Dispose(bool disposing)
		{
			DetachGlobalListener();

			base.Dispose(disposing);
		}
		#endregion
	}

	[ContractClassFor(typeof(LabController))]
	public abstract class LabControllerContract : LabController
	{
		protected override void MainMenu(ICollection<Lab> labs)
		{
			Contract.Requires(labs != null);
			Contract.Requires(labs.Count > 0);
			////Contract.Requires(Contract.ForAll(labs, lab => lab != null));
			////Contract.Requires(Contract.ForAll(labs, lab => lab.IsEnabled));
		}

		protected override void EndDebug(ICollection<Lab> labs)
		{
			Contract.Requires(labs != null);
			Contract.Requires(labs.Count > 0);
			////Contract.Requires(Contract.ForAll(labs, lab => lab != null));
			////Contract.Requires(Contract.ForAll(labs, lab => lab.IsEnabled));
		}

		protected override void TraceLabDescriptionIf(bool trace, string description)
		{
		}
	}
}